﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;

namespace ComputerBeacon.Csv {
    /// <summary>
    /// Serializes tabular data to and from csv format
    /// </summary>
    public static class Serialization {
        public enum EnclosureMode {
            Never,
            WhenNecessary,
            Always
        }

        class ValueField {
            public string Name;
            public FieldInfo FieldInfo;
            public PropertyInfo PropertyInfo;
        }

        static void WriteEnclosedString(StringBuilder sb, object o, EnclosureMode enclosureMode) {
            if (o == null) return;
            var s = o.ToString();

            bool enclose = enclosureMode == EnclosureMode.Always;
            if (enclosureMode == EnclosureMode.WhenNecessary) {
                for (int i = 0; i < s.Length; i++) {
                    if (s[i] == ',' || s[i] == '"') {
                        enclose = true;
                        break;
                    }
                }
            }

            if (!enclose) sb.Append(s);
            else {
                sb.Append('"');
                for (int i = 0; i < s.Length; i++) {
                    if (s[i] == '"') sb.Append("\"\"");
                    else sb.Append(s[i]);
                }
                sb.Append('"');
            }
        }

        /// <summary>
        /// Serializes data to csv format
        /// </summary>
        /// <typeparam name="T">Type of itmes to serialize</typeparam>
        /// <param name="items">items to serialize</param>
        /// <param name="skipReadonlyProperty">if true, read-only properties will not be serialized</param>
        /// <param name="enclosureMode">Whether to put quotes around values</param>
        /// <returns></returns>
        public static string Serialize<T>(IEnumerable<T> items, bool skipReadonlyProperty = true, EnclosureMode enclosureMode = EnclosureMode.WhenNecessary) {
            var sb = new StringBuilder();
            var columns = new List<ValueField>();
            foreach (var f in typeof(T).GetFields()) columns.Add(new ValueField { Name = f.Name, FieldInfo = f });
            foreach (var p in typeof(T).GetProperties()) {
                if (skipReadonlyProperty && !p.CanWrite) continue;
                columns.Add(new ValueField { Name = p.Name, PropertyInfo = p });
            }

            for (int i = 0; i < columns.Count; i++) {
                WriteEnclosedString(sb, columns[i].Name, enclosureMode);
                if (i != columns.Count - 1) sb.Append(',');
            }

            foreach (var t in items) {
                sb.Append("\r\n");
                for (int i = 0; i < columns.Count; i++) {
                    if (columns[i].FieldInfo != null) WriteEnclosedString(sb, columns[i].FieldInfo.GetValue(t), enclosureMode);
                    else WriteEnclosedString(sb, columns[i].PropertyInfo.GetValue(t, null), enclosureMode);
                    if (i != columns.Count - 1) sb.Append(',');
                }
            }

            return sb.ToString();
        }


        static Deserializer defaultDeserializer = new Deserializer();


        /// <summary>
        /// Deserialize data from a csv string
        /// </summary>
        /// <typeparam name="T">Type of instances to deserialize to</typeparam>
        /// <param name="csvString">data in csv format</param>
        /// <returns></returns>
        public static List<T> Deserialize<T>(string csvString) where T : new() {
            return Deserialize<T>(csvString, defaultDeserializer);
        }


        /// <summary>
        /// Deserialize data from a csv string
        /// </summary>
        /// <typeparam name="T">Type of instances to deserialize to</typeparam>
        /// <param name="csvString">data in csv format</param>
        /// <param name="deserializer">A deserialize which deserializes string values to the appropriate type</param>
        /// <returns></returns>
        public static List<T> Deserialize<T>(string csvString, Deserializer deserializer) where T : new() {
            var list = new List<T>();
            var scanner = new Scanner(csvString);
            var headers = new List<ValueField>();
            Scanner.Token token;
            
            do {
                token = scanner.Next();
                headers.Add(new ValueField { Name = token.Value });
            } while (token.NextType == Scanner.TokenType.String);

            for (int i = 0; i < headers.Count; i++) {
                headers[i].FieldInfo = typeof(T).GetField(headers[i].Name);
                if (headers[i].FieldInfo == null) headers[i].PropertyInfo = typeof(T).GetProperty(headers[i].Name);
            }

            while (token.NextType != Scanner.TokenType.End) {
                var t = new T();
                for (int i = 0; i < headers.Count; i++) {
                    token = scanner.Next();

                    if (i == 0 && token.NextType == Scanner.TokenType.End) goto exit; //allow trailing empty line
                    if ((i+1 == headers.Count) == (token.NextType == Scanner.TokenType.String)) throw new FormatException("Inconsistent column count in tabular data");
                    if (headers[i].FieldInfo != null)
                        headers[i].FieldInfo.SetValue(t, deserializer.Deserialize(headers[i].FieldInfo.FieldType, token.Value));
                    else if (headers[i].PropertyInfo != null && headers[i].PropertyInfo.CanWrite)
                        headers[i].PropertyInfo.SetValue(t, deserializer.Deserialize(headers[i].PropertyInfo.PropertyType, token.Value), null);
                }
                list.Add(t);
            }

            exit:
            return list;
        }
    }
}
